package meatslope2.gameengine.components;

import meatslope2.application.SystemRegistry;
import meatslope2.gameengine.engine.CollisionSystem;
import meatslope2.gameengine.gameobjects.model.MovableGameObject;
import meatslope2.gameengine.primatives.HitInfo;
import meatslope2.gameengine.primatives.Vector2;

//TODO: make sure within world

/**
 * On an update cycle this component will shoot a vertical ray toward the gameobject's
 * movement direction and you have the option of shooting one or two rays vertically for
 * more accuracy 
 * @author Mitchell Thelen
 *
 */
//TODO: might have to adjust vertical/horizontal rays - will have to see later
public class CollisionComponent extends Component
{
	// ===========================================================
	// Constants
	// ===========================================================
	public static final int SKINNY_MODE = 0; //cast one ray vertically
	public static final int FAT_MODE = 1; //cast two rays vertically
	
	// ===========================================================
	// Members
	// ===========================================================
	
	//Offsets used to add to player position in order to get the correct ray ending
	private final int[] offsetVerts; //we'll assume the offsets go from left to right
	
	private final int offsetX;
	private final int offsetY;
	
	// ===========================================================
	// Cached Members
	// ===========================================================
	
	//Caches the beginning of each ray
	private final Vector2 previousCenter = new Vector2();
	
	//Caches prevMovement - movement
	private final Vector2 deltaMovement = new Vector2();
	
	//Offset is used to compute the end point of the array for testPointEnd
	private final Vector2 testPointOffset = new Vector2();
	private final Vector2 testPointEnd = new Vector2();
	
	//Where to snap the player position to if hit
	private final Vector2 newPlayerOffset = new Vector2();
	
	//Where other objects can access the hit information from the last update cycle
	private final HitInfo verticalHit = new HitInfo();
	private final HitInfo horizontalHit = new HitInfo();
	
	// ===========================================================
	// Getters/Setters
	// ===========================================================	
	public HitInfo getVerticalHit()
	{
		return this.verticalHit;
	}
	
	public HitInfo getHorizontalHit()
	{
		return this.horizontalHit;
	}
		
	// ===========================================================
	// Constructors
	// ===========================================================
	/**
	 * By default, the collision component will be set to skinny mode
	 */
	public CollisionComponent(MovableGameObject gameObject)
	{
		this(gameObject, SKINNY_MODE);
	}
	
	public CollisionComponent(MovableGameObject gameObject, int collisionMode)
	{
		super(gameObject);
		
		this.offsetY = gameObject.getHeight() / 2;
		this.offsetX = gameObject.getWidth() / 2;
		
		if(collisionMode == SKINNY_MODE)
			this.offsetVerts = new int[]{ this.offsetX };
		else if(collisionMode == FAT_MODE)
		{
			final int quarterWidth = gameObject.getWidth() / 4;
			this.offsetVerts = new int[]{this.offsetX - quarterWidth, this.offsetX + quarterWidth};
		}
		else
			throw new IllegalArgumentException("collisionMode not equals to CollisionComponent.SKINNY_MODE or CollisionComponent.FAT_MODE");
	}
	
	// ===========================================================
	// Methods for/from SuperClass/Interfaces
	// ===========================================================
	@Override
	public void update()
	{
		//Be wary!  if you set these, you will affect the player!
		final Vector2 prevPlayerPosition = this.gameObject.getPrevPosition();
		final Vector2 playerPosition = this.gameObject.getPosition();
		
		//Don't run this on the first update cycle
		if(prevPlayerPosition.length2() == 0)
			return;
        
        deltaMovement.set(playerPosition);
        deltaMovement.subtract(prevPlayerPosition);
        
        previousCenter.set(this.offsetX, this.offsetY);
        previousCenter.add(prevPlayerPosition);
        
        //Reset our hitInfos and new player position before continuing
        verticalHit.reset();
        horizontalHit.reset();
        newPlayerOffset.zero();
        
        //Sweep horizontal
        if(deltaMovement.x > 0)
        {
        	//If we're moving right, go right to left
        	for(int i = offsetVerts.length - 1; i >= 0; i--)
        		if(sweepVertical(playerPosition, offsetVerts[i]))
        			break;
        }
        else
        {
        	//If we're moving left, go left to right
        	for(int i = 0; i < offsetVerts.length; i++)
        		if(sweepVertical(playerPosition, offsetVerts[i]))
        			break;
        }
        
        sweepHorizontal(playerPosition);
        
    	//Finally, adjust player position to new offset based off of collisions
    	//This must be computed at the end so both vertical and horizontal ray tests
    	//are done before moving the player position
        if(newPlayerOffset.x != 0)
        	playerPosition.x = newPlayerOffset.x;
        
        if(newPlayerOffset.y != 0)
        	playerPosition.y = newPlayerOffset.y;
	}

	// ===========================================================
	// Methods
	// ===========================================================
	
	/**
	 * Sweeps the space between two points looking for surfaces that oppose horizontal movement. 
	 **/
    protected void sweepHorizontal(Vector2 playerPosition) 
    {
    	if(this.deltaMovement.x == 0)
    		return;
    	
    	final CollisionSystem collisionSystem = SystemRegistry.collisionSystem;
    	
        // Shoot a ray from the center of the previous frame's box to the edge (left or right,
        // depending on the direction of movement) of the current box.
        this.testPointOffset.y = this.offsetY;
        this.testPointOffset.x = 0;
        if (this.deltaMovement.x > 0.0f)
        	this.testPointOffset.x = this.gameObject.getWidth();
        
        this.testPointEnd.set(playerPosition);
        this.testPointEnd.add(testPointOffset);
        
        HitInfo newHorizontalHit = collisionSystem.castRay(previousCenter, testPointEnd);
        this.horizontalHit.set(newHorizontalHit);
        
        //If we are moving right and hit something, we have to offset our hit-point by our character's width
        if(horizontalHit.isHit())
        	newPlayerOffset.x = horizontalHit.getHitPoint().x - testPointOffset.x;
    }
    
    /**
     * Sweeps the space between two points looking for surfaces that oppose vertical movement. 
     **/
    protected boolean sweepVertical(Vector2 playerPosition, int incomingOffset) 
    {
    	if(deltaMovement.y == 0)
    		return false;
    	
    	final CollisionSystem collisionSystem = SystemRegistry.collisionSystem;
    	
        // Shoot a ray from the center of the previous frame's box to the edge (top or bottom,
        // depending on the direction of movement) of the current box.
        this.testPointOffset.x = incomingOffset;
        this.testPointOffset.y = 0;
        if (deltaMovement.y > 0.0f) 
        	this.testPointOffset.y = this.gameObject.getHeight();
        
        this.testPointEnd.set(playerPosition);
        this.testPointEnd.add(testPointOffset);
        
        HitInfo newVerticalHit = collisionSystem.castRay(previousCenter, testPointEnd);
        this.verticalHit.set(newVerticalHit);
        
        //If we are moving right and hit something, we have to offset our hit-point by our character's height
        if(verticalHit.isHit())
        	newPlayerOffset.y = verticalHit.getHitPoint().y - this.testPointOffset.y;
        
        return verticalHit.isHit();
    }
}
